; ====================================================================================
; boot.asm
; 功能: 将 Loader 加载进入内存
; 编译方法：nasm boot.asm -o boot.bin
; 用 ubuntu 创建软盘映像文件（FAT12格式）：
; dd if=/dev/zero of=xlinix.img bs=512 count=2880   制作空白软盘映像
; sudo losetup -f   自动查找空闲的设备
; sudo losetup /dev/loop5 xlinix.img    关联回环设备，回环设备可以把文件虚拟成 Linux 块设备，用来模拟整个文件系统
; sudo losetup -d /dev/loop5            解除回环设备关联
; sudo mkfs.fat -F 12 /dev/loop5        格式化为 fat12（可以不关联回环设备，sudo mkfs.fat -F 12 xlinix.img）
; dd if=./Output/boot.bin of=xlinix.img bs=512 conv=notrunc(不截断剩余内容)  引导扇区写入软盘映像
; sudo mkdir -p ./Floppy    创建一个目录用来挂载软盘映像
; sudo mount -o loop xlinix.img ./Floppy 挂载软盘映像   （挂载时，没有手动设置回环设备系统也会自动设置）
; lsblk -f 或 mount -i  查看挂载是否成功
; sudo cp ./Output/loader.bin ./Floppy   复制文件到软盘映像
; sudo cp ./Output/kernel.bin ./Floppy   复制文件到软盘映像
; sudo umount ./Floppy    解除挂载
; hexdump -C xlinix.img | head -n 40    查看软盘前40*8字节
; ====================================================================================

;%define _BOOT_DEBUG_
%ifdef _BOOT_DEBUG_         
    org 0100h           ; 调试状态，做成 .com 文件
%else
    org 7c00h           ; 告诉编译器加载到7C00处
%endif

; 设置堆栈
%ifdef	_BOOT_DEBUG_
BaseOfStack		equ	0100h	; 调试状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%else
BaseOfStack		equ	07c00h	; Boot状态下堆栈基地址(栈底, 从这个位置向低地址生长)
%endif

%include    "load.inc"

; FAT12 引导扇区开始
    jmp short LABEL_START   ; Start to boot.
    nop         ; 这个 nop 不可少 可能是为了对齐，填充字节
                ; 这正是引导扇区 BS_jmpBoot 的格式

; 下面是 FAT12 磁盘的头
%include	"fat12hdr.inc"

LABEL_START:        
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, BaseOfStack

    ; 清屏
	mov	ax, 0600h		; AH = 6,  AL = 0h
	mov	bx, 0700h		; 黑底白字(BL = 07h)
	mov	cx, 0			; 左上角: (0, 0)
	mov	dx, 0184fh		; 右下角: (80, 50)
	int	10h			    ; int 10h

	mov	dh, 0			; "Booting  "
	call    DispStr     ; 显示字符串

    ; 软驱复位
    xor ah, ah
    xor dl, dl
    int 13h

; 下面在 A 盘的根目录寻找 LOADER.BIN
    mov word [wSectorNo], SectorNoOfRootDirectory
LABEL_SEARCH_IN_ROOT_DIR_BEGIN:
    cmp word [wRootDirSizeForLoop], 0   ; 判断根目录区是不是已经读完
    jz  LABEL_NO_LOADERBIN              ; 如果读完表示没有找到 LOADER.BIN
    dec word [wRootDirSizeForLoop]      
    mov ax, BaseOfLoader
    mov es, ax      ; es <- BaseOfLoader
    mov	bx, OffsetOfLoader	; bx <- OffsetOfLoader	
                            ; 于是, es:bx = BaseOfLoader:OffsetOfLoader
    mov ax, [wSectorNo]     ; ax <- Root Directory 中的某 Sector 号
    mov cl, 1
    call    ReadSector

    mov si, LoaderFileName  ; ds:si -> "LOADER  BIN"
    mov di, OffsetOfLoader  ; es:di -> BaseOfLoader:0100 = BaseOfLoader*10h+100
    cld         ; 清除方向标志位 低地址向高地址
    mov dx, 10h ; 一个扇区 512 字节，一个条目 32 字节，共 16 个条目
LABEL_SEARCH_FOR_LOADERBIN:
    cmp dx, 0                               ; 循环次数控制
    jz LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR   ; 如果已经读完一个 sector
    dec dx                                  ; 就跳到下一个 sector
    mov cx, 11
LABEL_CMP_FILENAME:
    cmp cx, 0
    jz  LABEL_FILENAME_FOUND    ; 如果比较了 11 个字符都相等, 表示找到
    dec cx
    lodsb       ; ds:si -> al
    cmp al, byte [es:di]
    jz  LABEL_GO_ON
    jmp LABEL_DIFFERENT     ; 只要发现不一样的字符就表明本 DirectoryEntry 不是
                            ; 我们要找的 LOADER.BIN
LABEL_GO_ON:
    inc di
    jmp LABEL_CMP_FILENAME  ; 继续循环

LABEL_DIFFERENT:
	and	di, 0FFE0h				    ; di &= E0 为了让它指向本条目开头 32 字节对齐
	add	di, 20h                     ; di += 20h  下一个目录条目
	mov	si, LoaderFileName             
	jmp	LABEL_SEARCH_FOR_LOADERBIN

LABEL_GOTO_NEXT_SECTOR_IN_ROOT_DIR:
	add	word [wSectorNo], 1         ; 扇区号加1
	jmp	LABEL_SEARCH_IN_ROOT_DIR_BEGIN

LABEL_NO_LOADERBIN:
	mov	dh, 2			; "No LOADER."
	call    DispStr     ; 显示字符串

%ifdef	_BOOT_DEBUG_
	mov	ax, 4c00h		; 调试模式时
	int	21h			    ; 没有找到 LOADER.BIN, 回到 DOS
%else
	jmp	$			    ; 没有找到 LOADER.BIN, 死循环在这里
%endif

LABEL_FILENAME_FOUND:   ; 找到 LOADER.BIN 后便来到这里继续
	mov ax, RootDirSectors
    and di, 0FFE0h      ; di -> 当前条目的开始（根目录中）
    add di, 01Ah        ; di -> 首 Sector（第26个字节开始的两个字节，表示此条目对应的开始簇号）
    mov cx, word [es:di]
    push    cx          ; 保存此 Sector 在 FAT 中的序号（对应数据区中的簇号）
    add cx, ax          ; 
    add cx, DeltaSectorNo   ; 这句完成时 cl 里面变成 LOADER.BIN 的起始扇区号 (从 0 开始数的序号)
	mov	ax, BaseOfLoader
    mov es, ax          ; es <- BaseOfLoader
	mov	bx, OffsetOfLoader	; bx <- OffsetOfLoader 于是, es:bx = BaseOfLoader:OffsetOfLoader = BaseOfLoader * 10h + OffsetOfLoader
	mov	ax, cx			; ax <- Sector 号

LABEL_GOON_LOADING_FILE:
    ; 每读一个扇区就在 "Booting  " 后面打一个点
    push    ax
    push    bx
    mov ah, 0Eh
    mov al, '.'
    mov bl, 0Fh
    int 10h             ; 显示
    pop bx
    pop ax

    mov cl, 1
    call    ReadSector
    pop ax              ; 取出此 Sector 在 FAT 中的序号
    call    GetFATEntry
    cmp ax, 0FFFh
    jz  LABEL_FILE_LOADED   ; FAT 项的值是0xFFF，最后一个簇
    push    ax          ; 保存下一个 Sector 在 FAT 中的序号
    mov dx, RootDirSectors
    add ax, dx
    add ax, DeltaSectorNo
    add bx, [BPB_BytsPerSec]    ; 刚开始 bx 位 0100h，读入一个扇区后，要加上一个扇区的大小（0200h，512字节）
    jmp LABEL_GOON_LOADING_FILE
LABEL_FILE_LOADED:
    mov dh, 1           ; "Ready."
    call    DispStr     ; 显示字符串

    jmp	BaseOfLoader:OffsetOfLoader ; 这一句正式跳转到已加载到内存中的 LOADER.BIN 的开始处
						; 开始执行 LOADER.BIN 的代码
						; Boot Sector 的使命到此结束

;============================================================================
;变量
;----------------------------------------------------------------------------
wRootDirSizeForLoop	dw	RootDirSectors	; Root Directory 占用的扇区数, 在循环中会递减至零.
wSectorNo		dw	0		; 要读取的扇区号
bOdd			db	0		; 奇数还是偶数

;============================================================================
;字符串
;----------------------------------------------------------------------------
LoaderFileName  db	"LOADER  BIN", 0	; LOADER.BIN 之文件名
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength   equ	9
BootMessage:    db	"Booting  " ; 9字节, 不够则用空格补齐. 序号 0
Message1		db	"Ready.   "     ; 9字节, 不够则用空格补齐. 序号 1
Message2		db	"No LOADER"     ; 9字节, 不够则用空格补齐. 序号 2
;============================================================================

;----------------------------------------------------------------------------
; 函数名: DispStr
;----------------------------------------------------------------------------
; 作用:
;	显示一个字符串, 函数开始时 dh 中应该是字符串序号(0-based)
DispStr:
    mov ax, MessageLength
    mul dh              ; dh 行号
    add ax, BootMessage ; ax = dh * MessageLength 即字符串偏移
    mov bp, ax          ; ┓
	mov	ax, ds			; ┣ ES:BP = 串地址
	mov	es, ax			; ┛
    mov cx, MessageLength   ; cx = 串长度
    mov ax, 1301h       ; ah = 13, al = 01h
    mov bx, 0007h       ; 页号为0（bh = 0） 黑底白字（bl = c，高4位0表示背景颜色，第四位c表示字符颜色）
    mov dl, 0           ; dl 列号
    int 10h             ; 10h 号中断
    ret

;----------------------------------------------------------------------------
; 函数名: ReadSector
;----------------------------------------------------------------------------
; 作用:
;	从第 ax 个 Sector 开始, 将 cl 个 Sector 读入 es:bx 中
ReadSector:
; -----------------------------------------------------------------------
	; 怎样由扇区号求扇区在磁盘中的位置 (扇区号 -> 柱面号, 起始扇区, 磁头号)
	; -----------------------------------------------------------------------
	; 设扇区号为 x
	;                           ┌ 柱面号 = y >> 1
	;       x           ┌ 商 y ┤
	; -------------- => ┤      └ 磁头号 = y & 1
	;  每磁道扇区数      │
	;                   └ 余 z => 起始扇区号 = z + 1
	push    bp
    mov bp, sp
    sub esp, 2      ; 辟出两个字节的堆栈区域保存要读的扇区数: byte [bp-2]
    mov byte [bp - 2], cl

    push    bx      ; 保存 bx
    mov bl, [BPB_SecPerTrk]     ; bl: 除数 每磁道扇区数
    div bl          ; y 在 al 中, z 在 ah 中
    inc ah          ; z++
    mov cl, ah      ; cl <- 起始扇区号
    mov dh, al      ; dh <- y
    shr	al, 1       ; y >> 1 (其实是 y/BPB_NumHeads, 这里BPB_NumHeads=2)
    mov	ch, al      ; ch <- 柱面号
    and dh, 1       ; dh & 1 = 磁头号
    pop bx          ; 恢复bx
    ; 至此, "柱面号, 起始扇区, 磁头号" 全部得到
    mov dl, [BS_DrvNum]         ; 驱动器号（0 表示 A 盘）
.GoOnReading:
    mov ah, 2       ; 读
    mov al, byte [bp - 2]       ; 读 al 个扇区
    int 13h 
    jc  .GoOnReading    ; 如果读取错误 CF 会被置为 1, 这时就不停地读, 直到正确为止

    add esp, 2
    pop bp

    ret

;----------------------------------------------------------------------------
; 函数名: GetFATEntry
;----------------------------------------------------------------------------
; 作用:
;	找到序号为 ax 的 Sector 在 FAT 中的条目, 结果放在 ax 中
;	需要注意的是, 中间需要读 FAT 的扇区到 es:bx 处, 所以函数一开始保存了 es 和 bx
GetFATEntry:
    push    es
    push    bx
    push    ax
    ; 在 BaseOfLoader 后面留出 4K 空间用于存放 FAT
    mov ax, BaseOfLoader
    sub ax, 0100h
    mov es, ax

    pop ax
    mov byte [bOdd], 0
    mov	bx, 3
	mul	bx			; dx:ax = ax * 3
	mov	bx, 2
	div	bx			; dx:ax / 2  ==>  ax <- 商, dx <- 余数
    cmp	dx, 0
	jz	LABEL_EVEN
	mov	byte [bOdd], 1
; 偶数
LABEL_EVEN:
    xor dx, dx      ; 现在 ax 中是 FATEntry 在 FAT 中的偏移量. 下面来计算 FATEntry 在哪个扇区中(FAT占用不止一个扇区)
    mov bx, [BPB_BytsPerSec]
    div bx          ; dx:ax / BPB_BytsPerSec ==> ax <- 商 (FATEntry 所在的扇区相对于 FAT 来说的扇区号)
					; dx <- 余数 (FATEntry 在扇区内的偏移)。
    push    dx      
    mov bx, 0       ; bx <- 0	于是, es:bx = (BaseOfLoader - 100):00 = (BaseOfLoader - 100) * 10h
    add ax, SectorNoOfFAT1  ; 此句执行之后的 ax 就是 FATEntry 所在的扇区号
    mov cl, 2
    call    ReadSector
    pop dx
    add bx, dx
    mov ax, [es:bx]
    cmp byte [bOdd], 1
    jnz LABEL_EVEN_2    ; 不等跳转指令，即偶数时跳转
    shr ax, 4

LABEL_EVEN_2:
    and ax, 0FFFh

LABEL_GET_FAT_ENRY_OK:
    pop bx
    pop es
    ret

times 510-($-$$)    db 0    ; 填充剩下的字节，使生成的二进制代码恰好为512字节
dw 0xaa55                   ; 结束标志